package gitbucket.core.controller

import gitbucket.core.issues.html
import gitbucket.core.service.IssuesService._
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import gitbucket.core.view
import gitbucket.core.view.Markdown
import io.github.gitbucket.scalatra.forms._
import org.scalatra.Ok


class IssuesController extends IssuesControllerBase
  with IssuesService
  with RepositoryService
  with AccountService
  with LabelsService
  with MilestonesService
  with ActivityService
  with HandleCommentService
  with ReadableUsersAuthenticator
  with ReferrerAuthenticator
  with WritableUsersAuthenticator
  with PullRequestService
  with WebHookIssueCommentService
  with CommitsService

trait IssuesControllerBase extends ControllerBase {
  self: IssuesService
    with RepositoryService
    with AccountService
    with LabelsService
    with MilestonesService
    with ActivityService
    with HandleCommentService
    with ReadableUsersAuthenticator
    with ReferrerAuthenticator
    with WritableUsersAuthenticator
    with PullRequestService
    with WebHookIssueCommentService =>

  case class IssueCreateForm(title: String, content: Option[String],
    assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
  case class CommentForm(issueId: Int, content: String)
  case class IssueStateForm(issueId: Int, content: Option[String])

  val issueCreateForm = mapping(
      "title"            -> trim(label("Title", text(required))),
      "content"          -> trim(optional(text())),
      "assignedUserName" -> trim(optional(text())),
      "milestoneId"      -> trim(optional(number())),
      "labelNames"       -> trim(optional(text()))
    )(IssueCreateForm.apply)

  val issueTitleEditForm = mapping(
    "title" -> trim(label("Title", text(required)))
    )(x => x)
  val issueEditForm = mapping(
    "content" -> trim(optional(text()))
    )(x => x)

  val commentForm = mapping(
      "issueId" -> label("Issue Id", number()),
      "content" -> trim(label("Comment", text(required)))
    )(CommentForm.apply)

  val issueStateForm = mapping(
      "issueId" -> label("Issue Id", number()),
      "content" -> trim(optional(text()))
    )(IssueStateForm.apply)

  get("/:owner/:repository/issues")(referrersOnly { repository =>
    val q = request.getParameter("q")
    if(Option(q).exists(_.contains("is:pr"))){
      redirect(s"/${repository.owner}/${repository.name}/pulls?q=" + StringUtil.urlEncode(q))
    } else {
      searchIssues(repository)
    }
  })

  get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
    defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
      getIssue(owner, name, issueId) map {
        html.issue(
          _,
          getComments(owner, name, issueId.toInt),
          getIssueLabels(owner, name, issueId.toInt),
          getAssignableUserNames(owner, name),
          getMilestonesWithIssueCount(owner, name),
          getLabels(owner, name),
          isEditable(repository),
          isManageable(repository),
          repository)
      } getOrElse NotFound()
    }
  })

  get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
    if(isEditable(repository)){ // TODO Should this check is provided by authenticator?
      defining(repository.owner, repository.name){ case (owner, name) =>
        html.create(
          getAssignableUserNames(owner, name),
          getMilestones(owner, name),
          getLabels(owner, name),
          isManageable(repository),
          repository)
      }
    } else Unauthorized()
  })

  post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
    if(isEditable(repository)){ // TODO Should this check is provided by authenticator?
      defining(repository.owner, repository.name){ case (owner, name) =>
        val manageable = isManageable(repository)
        val userName = context.loginAccount.get.userName

        // insert issue
        val issueId = createIssue(owner, name, userName, form.title, form.content,
          if (manageable) form.assignedUserName else None,
          if (manageable) form.milestoneId else None)

        // insert labels
        if (manageable) {
          form.labelNames.map { value =>
            val labels = getLabels(owner, name)
            value.split(",").foreach { labelName =>
              labels.find(_.labelName == labelName).map { label =>
                registerIssueLabel(owner, name, issueId, label.labelId)
              }
            }
          }
        }

        // record activity
        recordCreateIssueActivity(owner, name, userName, issueId, form.title)

        getIssue(owner, name, issueId.toString).foreach { issue =>
          // extract references and create refer comment
          createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get)

          // call web hooks
          callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get)

          // notifications
          Notifier().toNotify(repository, issue, form.content.getOrElse("")) {
            Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
          }
        }

        redirect(s"/${owner}/${name}/issues/${issueId}")
      }
    } else Unauthorized()
  })

  ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) =>
    defining(repository.owner, repository.name){ case (owner, name) =>
      getIssue(owner, name, params("id")).map { issue =>
        if(isEditableContent(owner, name, issue.openedUserName)){
          // update issue
          updateIssue(owner, name, issue.issueId, title, issue.content)
          // extract references and create refer comment
          createReferComment(owner, name, issue.copy(title = title), title, context.loginAccount.get)

          redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
        } else Unauthorized()
      } getOrElse NotFound()
    }
  })

  ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) =>
    defining(repository.owner, repository.name){ case (owner, name) =>
      getIssue(owner, name, params("id")).map { issue =>
        if(isEditableContent(owner, name, issue.openedUserName)){
          // update issue
          updateIssue(owner, name, issue.issueId, issue.title, content)
          // extract references and create refer comment
          createReferComment(owner, name, issue, content.getOrElse(""), context.loginAccount.get)

          redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
        } else Unauthorized()
      } getOrElse NotFound()
    }
  })

  post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
    getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
      val actionOpt = params.get("action").filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName))
      handleComment(issue, Some(form.content), repository, actionOpt) map { case (issue, id) =>
        redirect(s"/${repository.owner}/${repository.name}/${
          if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
      }
    } getOrElse NotFound()
  })

  post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
    getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
      val actionOpt = params.get("action").filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName))
      handleComment(issue, form.content, repository, actionOpt) map { case (issue, id) =>
        redirect(s"/${repository.owner}/${repository.name}/${
          if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
      }
    } getOrElse NotFound()
  })

  ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
    defining(repository.owner, repository.name){ case (owner, name) =>
      getComment(owner, name, params("id")).map { comment =>
        if(isEditableContent(owner, name, comment.commentedUserName)){
          updateComment(comment.commentId, form.content)
          redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
        } else Unauthorized()
      } getOrElse NotFound()
    }
  })

  ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository =>
    defining(repository.owner, repository.name){ case (owner, name) =>
      getComment(owner, name, params("id")).map { comment =>
        if(isEditableContent(owner, name, comment.commentedUserName)){
          Ok(deleteComment(comment.commentId))
        } else Unauthorized()
      } getOrElse NotFound()
    }
  })

  ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
    getIssue(repository.owner, repository.name, params("id")) map { x =>
      if(isEditableContent(x.userName, x.repositoryName, x.openedUserName)){
        params.get("dataType") collect {
          case t if t == "html" => html.editissue(x.content, x.issueId, repository)
        } getOrElse {
          contentType = formats("json")
          org.json4s.jackson.Serialization.write(
            Map(
              "title"   -> x.title,
              "content" -> Markdown.toHtml(
                markdown = x.content getOrElse "No description given.",
                repository = repository,
                enableWikiLink = false,
                enableRefsLink = true,
                enableAnchor = true,
                enableLineBreaks = true,
                enableTaskList = true,
                hasWritePermission = true
              )
            )
          )
        }
      } else Unauthorized()
    } getOrElse NotFound()
  })

  ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository =>
    getComment(repository.owner, repository.name, params("id")) map { x =>
      if(isEditableContent(x.userName, x.repositoryName, x.commentedUserName)){
        params.get("dataType") collect {
          case t if t == "html" => html.editcomment(x.content, x.commentId, repository)
        } getOrElse {
          contentType = formats("json")
          org.json4s.jackson.Serialization.write(
            Map(
              "content" -> view.Markdown.toHtml(
                markdown = x.content,
                repository = repository,
                enableWikiLink = false,
                enableRefsLink = true,
                enableAnchor = true,
                enableLineBreaks = true,
                enableTaskList = true,
                hasWritePermission = true
              )
            )
          )
        }
      } else Unauthorized()
    } getOrElse NotFound()
  })

  ajaxPost("/:owner/:repository/issues/new/label")(writableUsersOnly { repository =>
    val labelNames = params("labelNames").split(",")
    val labels = getLabels(repository.owner, repository.name).filter(x => labelNames.contains(x.labelName))
    html.labellist(labels)
  })

  ajaxPost("/:owner/:repository/issues/:id/label/new")(writableUsersOnly { repository =>
    defining(params("id").toInt){ issueId =>
      registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
      html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
    }
  })

  ajaxPost("/:owner/:repository/issues/:id/label/delete")(writableUsersOnly { repository =>
    defining(params("id").toInt){ issueId =>
      deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
      html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
    }
  })

  ajaxPost("/:owner/:repository/issues/:id/assign")(writableUsersOnly { repository =>
    updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
    Ok("updated")
  })

  ajaxPost("/:owner/:repository/issues/:id/milestone")(writableUsersOnly { repository =>
    updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
    milestoneId("milestoneId").map { milestoneId =>
      getMilestonesWithIssueCount(repository.owner, repository.name)
          .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
        gitbucket.core.issues.milestones.html.progress(openCount + closeCount, closeCount)
      } getOrElse NotFound()
    } getOrElse Ok()
  })

  post("/:owner/:repository/issues/batchedit/state")(writableUsersOnly { repository =>
    defining(params.get("value")){ action =>
      action match {
        case Some("open")  => executeBatch(repository) { issueId =>
          getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
            handleComment(issue, None, repository, Some("reopen"))
          }
        }
        case Some("close") => executeBatch(repository) { issueId =>
          getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
            handleComment(issue, None, repository, Some("close"))
          }
        }
        case _ => // TODO BadRequest
      }
    }
  })

  post("/:owner/:repository/issues/batchedit/label")(writableUsersOnly { repository =>
    params("value").toIntOpt.map{ labelId =>
      executeBatch(repository) { issueId =>
        getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
          registerIssueLabel(repository.owner, repository.name, issueId, labelId)
        }
      }
    } getOrElse NotFound()
  })

  post("/:owner/:repository/issues/batchedit/assign")(writableUsersOnly { repository =>
    defining(assignedUserName("value")){ value =>
      executeBatch(repository) {
        updateAssignedUserName(repository.owner, repository.name, _, value)
      }
    }
  })

  post("/:owner/:repository/issues/batchedit/milestone")(writableUsersOnly { repository =>
    defining(milestoneId("value")){ value =>
      executeBatch(repository) {
        updateMilestoneId(repository.owner, repository.name, _, value)
      }
    }
  })

  get("/:owner/:repository/_attached/:file")(referrersOnly { repository =>
    (Directory.getAttachedDir(repository.owner, repository.name) match {
      case dir if(dir.exists && dir.isDirectory) =>
        dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
          response.setHeader("Content-Disposition", f"""inline; filename=${file.getName}""")
          RawData(FileUtil.getMimeType(file.getName), file)
        }
      case _ => None
    }) getOrElse NotFound()
  })

  val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
  val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)

  private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
    params("checked").split(',') map(_.toInt) foreach execute
    params("from") match {
      case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues")
      case "pulls"  => redirect(s"/${repository.owner}/${repository.name}/pulls")
    }
  }

  private def searchIssues(repository: RepositoryService.RepositoryInfo) = {
    defining(repository.owner, repository.name){ case (owner, repoName) =>
      val page = IssueSearchCondition.page(request)

      // retrieve search condition
      val condition = IssueSearchCondition(request)

      html.list(
          "issues",
          searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
          page,
          getAssignableUserNames(owner, repoName),
          getMilestones(owner, repoName),
          getLabels(owner, repoName),
          countIssue(condition.copy(state = "open"  ), false, owner -> repoName),
          countIssue(condition.copy(state = "closed"), false, owner -> repoName),
          condition,
          repository,
          isEditable(repository),
          isManageable(repository))
    }
  }

  /**
   * Tests whether an logged-in user can manage issues.
   */
  private def isManageable(repository: RepositoryInfo)(implicit context: Context): Boolean = {
    hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
  }

  /**
   * Tests whether an logged-in user can post issues.
   */
  private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = {
    repository.repository.options.issuesOption match {
      case "ALL"     => !repository.repository.isPrivate && context.loginAccount.isDefined
      case "PUBLIC"  => hasGuestRole(repository.owner, repository.name, context.loginAccount)
      case "PRIVATE" => hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
      case "DISABLE" => false
    }
  }

  /**
   * Tests whether an issue or a comment is editable by a logged-in user.
   */
  private def isEditableContent(owner: String, repository: String, author: String)(implicit context: Context): Boolean = {
    hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
  }

}
